<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Sukin</title>
    <style>
      :root {
        --primary-color: #0d6efd;
        --primary-rgb: 13, 110, 253;
        --highlight-color: #dc3545;
        --highlight-rgb: 220, 53, 69;
        --bg-color-start: #f8f9fa;
        --bg-color-end: #f8f8f9;
        --text-color: #515a6e;
        --panel-bg-color: #ffffff;
        --border-color: #dee2e6;
      }
      * {
        box-sizing: border-box;
        user-select: none;
      }
      html,
      body {
        margin: 0;
        padding: 0;
        width: 100%;
        height: 100%;
        overflow: hidden;
        background-color: var(--bg-color-end);
        color: var(--text-color);
      }
      #background-layer {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background: radial-gradient(
          ellipse at bottom,
          var(--bg-color-start) 0%,
          var(--bg-color-end) 100%
        );
        background-size: cover;
        background-position: center;
        transition: background-image 0.5s ease-in-out;
      }
      #app-container,
      #scene-container {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
      }
      #ui-container {
        position: relative;
        z-index: 10;
        width: 100%;
        height: 100%;
        display: flex;
        flex-direction: column;
        justify-content: space-between;
        align-items: center;
        padding: 30px;
        pointer-events: none;
      }
      #history-container {
        display: none;
        position: absolute;
        top: 20px;
        left: 20px;
        z-index: 15;
        background-color: var(--panel-bg-color);
        border: 1px solid var(--border-color);
        border-radius: 10px;
        padding: 15px;
        max-width: 350px;
        max-height: 60%;
        overflow-y: auto;
        pointer-events: auto;
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
      }
      #history-container h3 {
        margin: 0 0 10px 0;
        padding-bottom: 10px;
        border-bottom: 1px solid var(--border-color);
        color: var(--text-color);
        text-align: center;
        font-size: 1rem;
      }
      #history-list {
        list-style: none;
        padding: 0;
        margin: 0;
        display: flex;
        flex-direction: column;
        gap: 15px;
      }
      .history-group-title {
        font-size: 0.8rem;
        color: #6c757d;
        margin-bottom: 5px;
        font-weight: bold;
      }
      .history-timestamp {
        font-size: 0.7rem;
        color: #6c757d;
        margin-left: 5px;
        font-weight: normal;
      }
      .history-names-container {
        display: flex;
        flex-wrap: wrap;
        gap: 8px;
      }
      .history-item {
        background-color: rgba(var(--highlight-rgb), 0.1);
        color: var(--highlight-color);
        padding: 4px 8px;
        border-radius: 4px;
        font-size: 0.9rem;
        border: 1px solid rgba(var(--highlight-rgb), 0.3);
      }
      #history-container::-webkit-scrollbar,
      .modal-content::-webkit-scrollbar {
        width: 6px;
      }
      #history-container::-webkit-scrollbar-track,
      .modal-content::-webkit-scrollbar-track {
        background: var(--bg-color-start);
      }
      #history-container::-webkit-scrollbar-thumb,
      .modal-content::-webkit-scrollbar-thumb {
        background: #ced4da;
        border-radius: 3px;
      }
      #history-container::-webkit-scrollbar-thumb:hover,
      .modal-content::-webkit-scrollbar-thumb:hover {
        background: #adb5bd;
      }
      .title {
        font-size: 2.5rem;
        font-weight: bold;
        color: var(--text-color);
        transition: color 0.3s ease;
        text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
      }
      .controls {
        display: flex;
        align-items: center;
        gap: 20px;
        pointer-events: auto;
      }
      .input-group {
        display: flex;
        align-items: center;
        gap: 10px;
        color: var(--text-color);
      }
      #num-to-draw-input {
        width: 60px;
        padding: 8px;
        font-size: 1rem;
        text-align: center;
        background-color: #fff;
        border: 1px solid var(--border-color);
        border-radius: 5px;
        color: var(--text-color);
        -moz-appearance: textfield;
      }
      #num-to-draw-input::-webkit-outer-spin-button,
      #num-to-draw-input::-webkit-inner-spin-button {
        -webkit-appearance: none;
        margin: 0;
      }
      .btn {
        padding: 12px 30px;
        font-size: 1.2rem;
        color: #fff;
        background-color: var(--primary-color);
        border: 1px solid var(--primary-color);
        border-radius: 8px;
        cursor: pointer;
        transition: all 0.2s ease;
        font-weight: 500;
        box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
      }
      .btn:hover {
        filter: brightness(1.1);
        box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
      }
      .btn:active {
        transform: scale(0.97);
      }
      .btn:disabled {
        color: #6c757d;
        background-color: #e9ecef;
        border-color: var(--border-color);
        cursor: not-allowed;
        box-shadow: none;
      }
      .btn-secondary {
        background-color: #6c757d;
        border-color: #6c757d;
      }
      .btn-danger {
        background-color: #dc3545;
        border-color: #dc3545;
      }
      #result-container {
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        z-index: 20;
        display: none;
        padding: 40px 60px;
        background-color: var(--panel-bg-color);
        border-radius: 15px;
        border: 1px solid var(--border-color);
        box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
        overflow: hidden;
      }
      #result-name {
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 4rem;
        font-weight: bold;
        color: var(--highlight-color);
        white-space: nowrap;
      }
      .name-card {
        display: inline-block;
        animation-duration: 0.4s;
        animation-fill-mode: forwards;
        animation-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
      }
      @keyframes slideInFromRight {
        from {
          transform: translateX(120%);
          opacity: 0;
        }
        to {
          transform: translateX(0);
          opacity: 1;
        }
      }
      @keyframes slideOutToLeft {
        from {
          transform: translateX(0);
          opacity: 1;
        }
        to {
          transform: translateX(-120%);
          opacity: 0;
        }
      }
      .modal-overlay {
        display: none;
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background-color: rgba(0, 0, 0, 0.4);
        z-index: 28;
        backdrop-filter: blur(4px);
      }
      .modal {
        display: none;
        position: fixed;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        z-index: 30;
        background-color: var(--panel-bg-color);
        border: 1px solid var(--border-color);
        border-radius: 15px;
        padding: 25px 35px;
        box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
        flex-direction: column;
        gap: 18px;
        pointer-events: auto;
        animation: fadeInScaleUp 0.3s;
      }
      #settings-panel {
        width: 800px;
        max-width: 90vw;
      }
      #create-dataset-modal {
        width: 500px;
        max-width: 90vw;
      }
      .modal-header h2 {
        margin: 0;
        padding-bottom: 15px;
        color: var(--text-color);
        text-align: center;
        font-size: 1.4rem;
        border-bottom: 1px solid var(--border-color);
      }
      .modal-content {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 40px;
      }
      .modal-column {
        display: flex;
        flex-direction: column;
        gap: 22px;
      }
      .modal-actions {
        display: flex;
        justify-content: flex-end;
        gap: 15px;
        margin-top: 20px;
      }
      .settings-section h3 {
        margin: 0;
        font-size: 1.1rem;
        color: #343a40;
        padding-bottom: 10px;
        border-bottom: 1px solid var(--border-color);
      }
      .setting-item {
        display: grid;
        grid-template-columns: 120px 1fr;
        align-items: center;
        gap: 15px;
        font-size: 1rem;
        margin-bottom: 8px;
      }
      .setting-item label {
        white-space: nowrap;
        flex-shrink: 0;
        color: #495057;
        font-weight: 500;
        text-align: left;
      }
      .setting-item input[type='text'],
      .setting-item select,
      .setting-item textarea {
        width: 100%;
        background-color: #f8f9fa;
        border: 1px solid var(--border-color);
        color: var(--text-color);
        padding: 8px;
        border-radius: 4px;
      }
      .setting-item input[type='color'] {
        width: 100%;
        height: 38px;
        padding: 4px;
        background-color: #fff;
        border: 1px solid var(--border-color);
        border-radius: 4px;
        cursor: pointer;
      }
      .setting-item textarea {
        min-height: 100px;
        resize: vertical;
      }
      .setting-item .btn-small {
        padding: 5px 10px;
        font-size: 0.9rem;
      }
      .button-group {
        display: flex;
        gap: 10px;
      }
      .setting-item input[type='range'] {
        -webkit-appearance: none;
        width: 100%;
        height: 8px;
        background: #ddd;
        border-radius: 5px;
        outline: none;
        opacity: 0.7;
        transition: opacity 0.2s;
        cursor: pointer;
      }
      .setting-item input[type='range']:hover {
        opacity: 1;
      }
      .setting-item input[type='range']::-webkit-slider-thumb {
        -webkit-appearance: none;
        appearance: none;
        width: 18px;
        height: 18px;
        background: var(--primary-color);
        border-radius: 50%;
        cursor: pointer;
      }
      .setting-item input[type='range']::-moz-range-thumb {
        width: 18px;
        height: 18px;
        background: var(--primary-color);
        border-radius: 50%;
        cursor: pointer;
      }
      @keyframes fadeInScaleUp {
        from {
          opacity: 0;
          transform: translate(-50%, -50%) scale(0.9);
        }
        to {
          opacity: 1;
          transform: translate(-50%, -50%) scale(1);
        }
      }
      .toggle-switch {
        position: relative;
        display: inline-block;
        width: 50px;
        height: 26px;
      }
      .toggle-switch input {
        opacity: 0;
        width: 0;
        height: 0;
      }
      .slider {
        position: absolute;
        cursor: pointer;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background-color: #ccc;
        transition: 0.4s;
        border-radius: 26px;
      }
      .slider:before {
        position: absolute;
        content: '';
        height: 20px;
        width: 20px;
        left: 3px;
        bottom: 3px;
        background-color: white;
        transition: 0.4s;
        border-radius: 50%;
        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
      }
      input:checked + .slider {
        background-color: var(--primary-color);
      }
      input:checked + .slider:before {
        transform: translateX(24px);
      }
      #settings-btn {
        position: absolute;
        top: 20px;
        right: 20px;
        z-index: 25;
        width: 40px;
        height: 40px;
        background: none;
        border: none;
        cursor: pointer;
        pointer-events: auto;
        transition: transform 0.3s ease;
      }
      #settings-btn:hover {
        transform: rotate(90deg);
      }
      #settings-btn svg {
        width: 100%;
        height: 100%;
        fill: #6c757d;
      }
      @media (max-width: 850px) {
        #settings-panel {
          max-height: 85vh;
        }
        .modal-content {
          grid-template-columns: 1fr;
          gap: 20px;
          overflow-y: auto;
          padding: 5px 10px 10px 5px;
        }
        #settings-panel {
          width: 480px;
        }
      }
      @media (max-width: 768px) {
        .title {
          font-size: 1.8rem;
        }
        .btn {
          padding: 10px 20px;
          font-size: 1rem;
        }
        #result-name {
          font-size: 2.5rem;
        }
        .controls {
          flex-direction: column;
        }
        #settings-panel,
        #create-dataset-modal {
          width: 98vw;
        }
      }
      #create-dataset-modal .modal-content {
        display: grid !important;
        grid-template-columns: 1fr;
        gap: 18px;
      }
      #create-dataset-modal .setting-item {
        display: grid;
        grid-template-columns: 120px 1fr;
        align-items: center;
        gap: 15px;
        font-size: 1rem;
        margin-bottom: 8px;
      }
      #create-dataset-modal .setting-item label {
        text-align: right;
      }
    </style>
    <script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
  </head>
  <body>
    <div id="background-layer"></div>
    <div id="app-container">
      <div id="scene-container"></div>
      <div id="history-container">
        <h3>抽取历史</h3>
        <ul id="history-list"></ul>
      </div>
      <button id="settings-btn" title="设置">
        <svg viewBox="0 0 24 24">
          <path
            d="M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69-.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98s.03.66.07.98l-2.11 1.65c-.19-.15-.24-.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69-.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18-.49.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61.22l2-3.46c.12-.22.07-.49-.12.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"
          />
        </svg>
      </button>
      <div id="modal-overlay"></div>
      <div id="settings-panel" class="modal">
        <div class="modal-header"><h2>设置</h2></div>
        <div class="modal-content">
          <div class="modal-column">
            <div class="settings-section">
              <h3>数据管理</h3>
              <div class="setting-item">
                <label for="dataset-select">当前数据集</label>
                <select id="dataset-select"></select>
              </div>
              <div class="setting-item">
                <label></label>
                <div class="button-group">
                  <button id="set-default-dataset-btn" class="btn btn-small">设为默认</button>
                  <button id="open-create-dataset-modal-btn" class="btn btn-small">
                    创建新数据集
                  </button>
                  <button id="delete-dataset-btn" class="btn btn-danger btn-small">删除选中</button>
                </div>
              </div>
            </div>
            <hr class="separator" />
            <div class="settings-section">
              <h3>会话与历史</h3>
              <div class="setting-item">
                <label for="session-title-input">当前抽取标题</label>
                <input type="text" id="session-title-input" />
              </div>
              <div class="setting-item">
                <label for="history-session-select">加载历史会话</label>
                <select id="history-session-select"></select>
              </div>
              <div class="setting-item">
                <label>保存历史到本地</label>
                <label class="toggle-switch"
                  ><input type="checkbox" id="persist-toggle" /><span class="slider"></span
                ></label>
              </div>
              <div class="setting-item">
                <label></label>
                <button id="delete-session-btn" class="btn btn-danger btn-small">
                  删除当前会话
                </button>
              </div>
            </div>
          </div>
          <div class="modal-column">
            <div class="settings-section">
              <h3>外观与功能</h3>
              <div class="setting-item">
                <label for="speech-toggle">语音播报</label>
                <label class="toggle-switch"
                  ><input type="checkbox" id="speech-toggle" /><span class="slider"></span
                ></label>
              </div>
              <div class="setting-item">
                <label for="glow-toggle">字体荧光</label>
                <label class="toggle-switch"
                  ><input type="checkbox" id="glow-toggle" /><span class="slider"></span
                ></label>
              </div>
              <div class="setting-item">
                <label for="rotation-speed-slider">旋转速度</label>
                <input type="range" id="rotation-speed-slider" min="10" max="3000" value="100" />
              </div>
              <div class="setting-item">
                <label for="irregular-rotation-toggle">图形变幻</label>
                <label class="toggle-switch"
                  ><input type="checkbox" id="irregular-rotation-toggle" /><span
                    class="slider"
                  ></span
                ></label>
              </div>
              <div class="setting-item">
                <label for="transformation-speed-slider">变幻间隔(ms)</label>
                <input
                  type="range"
                  id="transformation-speed-slider"
                  min="500"
                  max="8000"
                  value="3000"
                />
              </div>
              <div class="setting-item">
                <label for="unpicked-color-picker">未抽中颜色</label>
                <input type="color" id="unpicked-color-picker" />
              </div>
              <div class="setting-item">
                <label for="picked-color-picker">已抽中颜色</label>
                <input type="color" id="picked-color-picker" />
              </div>
              <div class="setting-item">
                <label for="bg-color-picker">背景颜色</label>
                <input type="color" id="bg-color-picker" />
              </div>
              <div class="setting-item">
                <label>背景图片</label>
                <div class="button-group">
                  <button
                    class="btn btn-small"
                    onclick="document.getElementById('bg-image-input').click()"
                  >
                    选择文件
                  </button>
                  <button id="remove-bg-btn" class="btn btn-small">移除图片</button>
                </div>
                <input type="file" id="bg-image-input" accept="image/*" style="display: none" />
              </div>
              <hr class="separator" />
              <div class="setting-item">
                <label></label>
                <button id="reset-settings-btn" class="btn btn-secondary">恢复默认设置</button>
              </div>
            </div>
          </div>
        </div>
        <div class="modal-actions">
          <button id="cancel-settings-btn" class="btn btn-secondary">取消</button>
          <button id="save-settings-btn" class="btn">保存并关闭</button>
        </div>
      </div>
      <div id="create-dataset-modal" class="modal">
        <div class="modal-header"><h2>创建新数据集</h2></div>
        <div class="modal-content">
          <div class="setting-item">
            <label for="new-dataset-name-input">数据集名称</label>
            <input type="text" id="new-dataset-name-input" placeholder="例如：项目组成员" />
          </div>
          <div class="setting-item" style="align-items: flex-start">
            <label for="new-dataset-names-input">名单</label>
            <textarea
              id="new-dataset-names-input"
              placeholder="请用空格或换行分隔每个名字..."
            ></textarea>
          </div>
          <div class="setting-item" style="grid-template-columns: 120px 1fr; align-items: start">
            <label>从Excel导入</label>
            <div>
              <input
                type="file"
                id="xlsx-file-input"
                accept=".xlsx,.xls"
                style="width: 100%; margin-bottom: 8px"
              />
              <input
                type="text"
                id="xlsx-column-input"
                placeholder="读取列, 如: A,B"
                style="width: 100%; margin-bottom: 8px"
              />
              <input
                type="text"
                id="xlsx-filter-input"
                placeholder="过滤词, 如: 姓名,学号"
                style="width: 100%; margin-bottom: 8px"
              />
              <button id="import-xlsx-btn" class="btn btn-small">导入并填充上方文本框</button>
            </div>
          </div>
        </div>
        <div class="modal-actions">
          <button id="cancel-create-dataset-btn" class="btn btn-secondary">取消</button>
          <button id="create-dataset-btn" class="btn">创建</button>
        </div>
      </div>
      <div id="ui-container">
        <h1 class="title">随机点名</h1>
        <div id="result-container">
          <div id="result-name-wrapper">
            <div id="result-name"></div>
          </div>
        </div>
        <div class="controls">
          <div class="input-group">
            <label for="num-to-draw-input">抽取人数:</label>
            <input type="number" id="num-to-draw-input" value="1" min="1" />
          </div>
          <button id="start-btn" class="btn">开始</button>
          <button id="stop-btn" class="btn" disabled>停止</button>
          <button id="reset-btn" class="btn">重置</button>
        </div>
      </div>
    </div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/tween.js/18.6.4/tween.umd.js"></script>
    <script>
      const DOM = {
        sceneContainer: document.getElementById('scene-container'),
        startBtn: document.getElementById('start-btn'),
        stopBtn: document.getElementById('stop-btn'),
        resetBtn: document.getElementById('reset-btn'),
        resultContainer: document.getElementById('result-container'),
        resultName: document.getElementById('result-name'),
        historyList: document.getElementById('history-list'),
        historyContainer: document.getElementById('history-container'),
        numToDrawInput: document.getElementById('num-to-draw-input'),
        backgroundLayer: document.getElementById('background-layer'),
        settingsBtn: document.getElementById('settings-btn'),
        settingsPanel: document.getElementById('settings-panel'),
        saveSettingsBtn: document.getElementById('save-settings-btn'),
        cancelSettingsBtn: document.getElementById('cancel-settings-btn'),
        speechToggle: document.getElementById('speech-toggle'),
        glowToggle: document.getElementById('glow-toggle'),
        unpickedColorPicker: document.getElementById('unpicked-color-picker'),
        pickedColorPicker: document.getElementById('picked-color-picker'),
        bgColorPicker: document.getElementById('bg-color-picker'),
        bgImageInput: document.getElementById('bg-image-input'),
        removeBgBtn: document.getElementById('remove-bg-btn'),
        resetSettingsBtn: document.getElementById('reset-settings-btn'),
        sessionTitleInput: document.getElementById('session-title-input'),
        persistToggle: document.getElementById('persist-toggle'),
        historySessionSelect: document.getElementById('history-session-select'),
        deleteSessionBtn: document.getElementById('delete-session-btn'),
        datasetSelect: document.getElementById('dataset-select'),
        deleteDatasetBtn: document.getElementById('delete-dataset-btn'),
        setDefaultDatasetBtn: document.getElementById('set-default-dataset-btn'),
        openCreateDatasetModalBtn: document.getElementById('open-create-dataset-modal-btn'),
        modalOverlay: document.getElementById('modal-overlay'),
        createDatasetModal: document.getElementById('create-dataset-modal'),
        newDatasetNameInput: document.getElementById('new-dataset-name-input'),
        newDatasetNamesInput: document.getElementById('new-dataset-names-input'),
        createDatasetBtn: document.getElementById('create-dataset-btn'),
        cancelCreateDatasetBtn: document.getElementById('cancel-create-dataset-btn'),
        xlsxFileInput: document.getElementById('xlsx-file-input'),
        xlsxColumnInput: document.getElementById('xlsx-column-input'),
        xlsxFilterInput: document.getElementById('xlsx-filter-input'),
        importXlsxBtn: document.getElementById('import-xlsx-btn'),
        title: document.querySelector('.title'),
        rotationSpeedSlider: document.getElementById('rotation-speed-slider'),
        irregularRotationToggle: document.getElementById('irregular-rotation-toggle'),
        transformationSpeedSlider: document.getElementById('transformation-speed-slider'),
      }
      let scene,
        camera,
        renderer,
        nameSprites = [],
        sphereGroup
      let state = 'idle'
      let pickedNames = new Set()
      let historyLog = []
      let allSessions = {}
      let currentSessionTitle = '默认会话'
      let allDatasets = {}
      let currentDatasetName = ''
      let currentNames = []
      let tempSettings = {}
      let formationChangeInterval = null
      let currentFormationIndex = 0
      let formations = []
      const rotationSound = new Audio('./video.mp4')
      rotationSound.loop = true
      const resultSound = new Audio('./video.mp4')
      const PRESET_DATASETS = {
        21: {
          isPreset: true,
          names: [],
        },
        20: {
          isPreset: true,
          names: [],
        },
      }
      const defaultSettings = {
        speechEnabled: true,
        glowEnabled: false,
        unpickedColor: '#212529',
        pickedColor: '#dc3545',
        bgColor: '#e9ecef',
        bgImage: 'none',
        persistHistory: true,
        defaultDataset: Object.keys(PRESET_DATASETS)[0],
        rotationSpeed: 100,
        irregularRotation: false,
        transformationSpeed: 3000,
      }
      let settings = {...defaultSettings}
      const easingFunctions = [
        TWEEN.Easing.Exponential.InOut,
        TWEEN.Easing.Elastic.Out,
        TWEEN.Easing.Back.InOut,
        TWEEN.Easing.Quintic.InOut,
        TWEEN.Easing.Bounce.Out,
      ]
      function formSphere(i, count) {
        const radius = 10
        const phi = Math.acos(-1 + (2 * i) / count)
        const theta = Math.sqrt(count * Math.PI) * phi
        return new THREE.Vector3().setFromSphericalCoords(radius, phi, theta)
      }
      function formPlane(i, count) {
        const side = Math.ceil(Math.sqrt(count))
        const x = (i % side) - (side - 1) / 2
        const y = Math.floor(i / side) - (side - 1) / 2
        return new THREE.Vector3(x * 2.5, y * 2.5, 0)
      }
      function formRing(i, count) {
        const radius = 12
        const angle = (i / count) * Math.PI * 2
        return new THREE.Vector3(Math.cos(angle) * radius, Math.sin(angle) * radius, 0)
      }
      function formHelix(i, count) {
        const radius = 6
        const angle = (i / count) * Math.PI * 10
        const y = (i / count - 0.5) * 20
        return new THREE.Vector3(Math.cos(angle) * radius, y, Math.sin(angle) * radius)
      }
      function formTorusKnot(i, count) {
        const radius = 8
        const tube = 3.5
        const p = 2
        const q = 3
        const u = (i / count) * 2 * Math.PI * q
        const x = (radius + tube * Math.cos((p * u) / q)) * Math.cos(u)
        const y = (radius + tube * Math.cos((p * u) / q)) * Math.sin(u)
        const z = -tube * Math.sin((p * u) / q)
        return new THREE.Vector3(x, y, z)
      }
      formations = [formSphere, formRing, formPlane, formHelix, formTorusKnot]
      function changeFormation() {
        if (state !== 'rotating') return
        currentFormationIndex = (currentFormationIndex + 1) % formations.length
        const formationFn = formations[currentFormationIndex]
        const easingFn = easingFunctions[currentFormationIndex % easingFunctions.length]
        const duration = Math.max(500, settings.transformationSpeed * 0.5)
        const finalScale = {x: 4, y: 2, z: 1}
        nameSprites.forEach((sprite, i) => {
          const targetPosition = formationFn(i, nameSprites.length)
          sprite.scale.set(0.1, 0.1, 0.1)
          new TWEEN.Tween(sprite.position).to(targetPosition, duration).easing(easingFn).start()
          new TWEEN.Tween(sprite.scale).to(finalScale, duration).easing(easingFn).start()
        })
      }
      function hexToRgb(hex) {
        let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
        return result
          ? `${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}`
          : '0,0,0'
      }
      function lightenHexColor(hex, amount) {
        let usePound = false
        if (hex[0] == '#') {
          hex = hex.slice(1)
          usePound = true
        }
        let num = parseInt(hex, 16)
        let r = (num >> 16) + amount
        if (r > 255) r = 255
        else if (r < 0) r = 0
        let b = ((num >> 8) & 0x00ff) + amount
        if (b > 255) b = 255
        else if (b < 0) b = 0
        let g = (num & 0x0000ff) + amount
        if (g > 255) g = 255
        else if (g < 0) g = 0
        return (usePound ? '#' : '') + (g | (b << 8) | (r << 16)).toString(16).padStart(6, '0')
      }
      function updateTheme() {
        const root = document.documentElement
        root.style.setProperty('--primary-color', '#0d6efd')
        root.style.setProperty('--highlight-color', settings.pickedColor)
        root.style.setProperty('--highlight-rgb', hexToRgb(settings.pickedColor))
        root.style.setProperty('--bg-color-end', settings.bgColor)
        root.style.setProperty('--bg-color-start', lightenHexColor(settings.bgColor, 20))
        DOM.title.style.color = settings.unpickedColor
        updateNameSpritesState()
      }
      function createNameTexture(text, isPicked) {
        const canvas = document.createElement('canvas')
        const context = canvas.getContext('2d')
        const fontSize = 48
        canvas.width = 256
        canvas.height = 128
        context.font = `bold ${fontSize}px Microsoft YaHei`
        context.fillStyle = isPicked ? settings.pickedColor : settings.unpickedColor
        if (settings.glowEnabled) {
          context.shadowColor = isPicked
            ? `rgba(${hexToRgb(settings.pickedColor)}, 0.8)`
            : `rgba(${hexToRgb(settings.unpickedColor)}, 0.8)`
          context.shadowBlur = 15
        }
        context.textAlign = 'center'
        context.textBaseline = 'middle'
        context.fillText(text, canvas.width / 2, canvas.height / 2)
        return new THREE.CanvasTexture(canvas)
      }
      function initScene() {
        scene = new THREE.Scene()
        camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
        camera.position.z = 25
        renderer = new THREE.WebGLRenderer({antialias: true, alpha: true})
        renderer.setSize(window.innerWidth, window.innerHeight)
        DOM.sceneContainer.innerHTML = ''
        DOM.sceneContainer.appendChild(renderer.domElement)
        window.addEventListener('resize', onWindowResize, false)
      }
      function rebuildScene() {
        if (sphereGroup && scene) scene.remove(sphereGroup)
        sphereGroup = new THREE.Group()
        scene.add(sphereGroup)
        nameSprites = []
        const screenBounds = {x: (window.innerWidth / window.innerHeight) * 12, y: 12}
        currentNames.forEach((name, i) => {
          const isPicked = pickedNames.has(name)
          const texture = createNameTexture(name, isPicked)
          const material = new THREE.SpriteMaterial({map: texture})
          const sprite = new THREE.Sprite(material)
          sprite.scale.set(4, 2, 1)
          sprite.userData = {
            name,
            isPicked,
            velocity: new THREE.Vector3(
              (Math.random() - 0.5) * 0.02,
              (Math.random() - 0.5) * 0.02,
              0
            ),
            bounds: screenBounds,
            index: i,
          }
          sprite.position.set(
            (Math.random() - 0.5) * screenBounds.x * 1.8,
            (Math.random() - 0.5) * screenBounds.y * 1.8,
            0
          )
          nameSprites.push(sprite)
          sphereGroup.add(sprite)
        })
        returnToIdleState()
      }
      function animate() {
        requestAnimationFrame(animate)
        TWEEN.update()
        if (state === 'idle' && nameSprites.length > 0) {
          nameSprites.forEach(sprite => {
            sprite.position.add(sprite.userData.velocity)
            const {x, y} = sprite.position
            const {bounds, velocity} = sprite.userData
            if (x > bounds.x || x < -bounds.x) velocity.x *= -1
            if (y > bounds.y || y < -bounds.y) velocity.y *= -1
          })
        }
        if (state === 'rotating') {
          const baseSpeed = (settings.rotationSpeed / 100) * 0.01
          sphereGroup.rotation.x += baseSpeed * 0.1
          sphereGroup.rotation.y += baseSpeed * 0.2
        }
        if (renderer) renderer.render(scene, camera)
      }
      function onWindowResize() {
        if (!camera || !renderer) return
        camera.aspect = window.innerWidth / window.innerHeight
        camera.updateProjectionMatrix()
        renderer.setSize(window.innerWidth, window.innerHeight)
        const screenBounds = {x: (window.innerWidth / window.innerHeight) * 12, y: 12}
        nameSprites.forEach(s => (s.userData.bounds = screenBounds))
      }
      function updateButtonStates(isAnimating, isResultShown) {
        DOM.startBtn.disabled = isAnimating || isResultShown
        DOM.stopBtn.disabled = !isAnimating || isResultShown
        DOM.resetBtn.disabled = isAnimating || isResultShown
      }
      function returnToIdleState() {
        state = 'idle'
        clearInterval(formationChangeInterval)
        rotationSound.pause()
        rotationSound.currentTime = 0
        updateButtonStates(false, false)
        DOM.resultContainer.style.display = 'none'
        updateNameSpritesState()
        nameSprites.forEach(sprite => {
          sprite.visible = true
          const targetPosition = new THREE.Vector3(
            (Math.random() - 0.5) * sprite.userData.bounds.x * 1.8,
            (Math.random() - 0.5) * sprite.userData.bounds.y * 1.8,
            0
          )
          new TWEEN.Tween(sprite.position)
            .to(targetPosition, 1000)
            .easing(TWEEN.Easing.Exponential.Out)
            .start()
        })
      }
      function speakNames(namesToSpeak, onComplete) {
        if (!settings.speechEnabled || namesToSpeak.length === 0) {
          if (onComplete) onComplete()
          return
        }
        let index = 0
        function speakNext() {
          if (index >= namesToSpeak.length) {
            if (onComplete) onComplete()
            return
          }
          const utterance = new SpeechSynthesisUtterance(namesToSpeak[index])
          utterance.lang = 'zh-CN'
          utterance.rate = 1.1
          utterance.onend = () => speakNext()
          speechSynthesis.speak(utterance)
          index++
        }
        speakNext()
      }
      function applyAndSaveTempSettings() {
        settings = JSON.parse(JSON.stringify(tempSettings))
        localStorage.setItem('namePickerSettings', JSON.stringify(settings))
        applySettingsUI()
      }
      function applySettingsUI() {
        DOM.backgroundLayer.style.backgroundImage =
          settings.bgImage === 'none' ? '' : `url(${settings.bgImage})`
        updateTheme()
      }
      function updatePanelFromTempSettings() {
        DOM.speechToggle.checked = tempSettings.speechEnabled
        DOM.glowToggle.checked = tempSettings.glowEnabled
        DOM.unpickedColorPicker.value = tempSettings.unpickedColor
        DOM.pickedColorPicker.value = tempSettings.pickedColor
        DOM.bgColorPicker.value = tempSettings.bgColor
        DOM.persistToggle.checked = tempSettings.persistHistory
        DOM.rotationSpeedSlider.value = tempSettings.rotationSpeed
        DOM.irregularRotationToggle.checked = tempSettings.irregularRotation
        DOM.transformationSpeedSlider.value = tempSettings.transformationSpeed
      }
      function loadAllDatasets() {
        allDatasets = JSON.parse(JSON.stringify(PRESET_DATASETS))
        const customDatasets = localStorage.getItem('namePickerCustomDatasets')
        if (customDatasets) Object.assign(allDatasets, JSON.parse(customDatasets))
        populateDatasetSelect()
      }
      function saveCustomDatasets() {
        const custom = {}
        for (const key in allDatasets) {
          if (!allDatasets[key].isPreset) custom[key] = allDatasets[key]
        }
        localStorage.setItem('namePickerCustomDatasets', JSON.stringify(custom))
      }
      function setCurrentDataset(name) {
        if (!allDatasets[name]) name = Object.keys(PRESET_DATASETS)[0]
        currentDatasetName = name
        currentNames = allDatasets[name].names
        DOM.datasetSelect.value = name
        DOM.title.textContent = name
        loadAllSessions()
        updateDatasetButtons()
      }
      function populateDatasetSelect() {
        DOM.datasetSelect.innerHTML = ''
        Object.keys(allDatasets).forEach(title => {
          const option = document.createElement('option')
          option.value = title
          option.textContent = title
          DOM.datasetSelect.appendChild(option)
        })
      }
      function updateDatasetButtons() {
        DOM.deleteDatasetBtn.disabled = allDatasets[currentDatasetName]?.isPreset
        DOM.setDefaultDatasetBtn.disabled = currentDatasetName === settings.defaultDataset
      }
      function loadAllSessions() {
        let allSessionsData = {}
        if (settings.persistHistory) {
          const data = localStorage.getItem('namePickerAllSessions')
          allSessionsData = data ? JSON.parse(data) : {}
        }
        allSessions = allSessionsData[currentDatasetName] || {}
        if (!allSessions[currentSessionTitle]) currentSessionTitle = '默认会话'
        populateHistorySelect()
        loadSession(currentSessionTitle)
      }
      function saveAllSessions() {
        if (settings.persistHistory) {
          let allSessionsData = {}
          const data = localStorage.getItem('namePickerAllSessions')
          if (data) allSessionsData = JSON.parse(data)
          allSessionsData[currentDatasetName] = allSessions
          localStorage.setItem('namePickerAllSessions', JSON.stringify(allSessionsData))
        }
      }
      function loadSession(title, datasetChanged = false) {
        currentSessionTitle = title
        DOM.sessionTitleInput.value = title
        DOM.historySessionSelect.value = title
        historyLog = allSessions[title] || []
        pickedNames.clear()
        historyLog.forEach(drawEvent => {
          drawEvent.names.forEach(name => pickedNames.add(name))
        })
        updateHistoryUI()
        updateNumToDrawInputState()
        rebuildScene()
      }
      function populateHistorySelect() {
        DOM.historySessionSelect.innerHTML = ''
        if (Object.keys(allSessions).length === 0) allSessions['默认会话'] = []
        Object.keys(allSessions).forEach(title => {
          const option = document.createElement('option')
          option.value = title
          option.textContent = title
          DOM.historySessionSelect.appendChild(option)
        })
        DOM.historySessionSelect.value = currentSessionTitle
      }
      function updateNameSpritesState() {
        nameSprites.forEach(sprite => {
          const isPicked = pickedNames.has(sprite.userData.name)
          if (sprite.userData.isPicked !== isPicked) {
            sprite.userData.isPicked = isPicked
            sprite.material.map = createNameTexture(sprite.userData.name, isPicked)
            sprite.material.needsUpdate = true
          }
        })
      }
      function updateHistoryUI() {
        DOM.historyContainer.style.display = historyLog.length > 0 ? 'block' : 'none'
        DOM.historyList.innerHTML = ''
        for (let i = historyLog.length - 1; i >= 0; i--) {
          const drawEvent = historyLog[i]
          const groupLi = document.createElement('li')
          const title = document.createElement('div')
          title.className = 'history-group-title'
          const time = new Date(drawEvent.time).toLocaleString('zh-CN', {
            month: 'short',
            day: 'numeric',
            hour: '2-digit',
            minute: '2-digit',
          })
          title.innerHTML = `第 ${i + 1} 次 <span class="history-timestamp">(${time})</span>`
          groupLi.appendChild(title)
          const namesContainer = document.createElement('div')
          namesContainer.className = 'history-names-container'
          drawEvent.names.forEach(name => {
            const nameSpan = document.createElement('span')
            nameSpan.className = 'history-item'
            nameSpan.textContent = name
            namesContainer.appendChild(nameSpan)
          })
          groupLi.appendChild(namesContainer)
          DOM.historyList.appendChild(groupLi)
        }
      }
      function showWinnersSequentially(winnerNames, index = 0) {
        if (index >= winnerNames.length) {
          DOM.resultContainer.style.display = 'none'
          setTimeout(returnToIdleState, 500)
          return
        }
        const currentName = winnerNames[index]
        DOM.resultName.innerHTML = ''
        const nameEl = document.createElement('div')
        nameEl.className = 'name-card'
        nameEl.textContent = currentName
        DOM.resultName.appendChild(nameEl)
        DOM.resultContainer.style.display = 'block'
        nameEl.style.animationName = 'slideInFromRight'
        resultSound.currentTime = 0
        resultSound.play().catch(e => {})
        speakNames([currentName], () => {
          setTimeout(() => {
            nameEl.style.animationName = 'slideOutToLeft'
            setTimeout(() => showWinnersSequentially(winnerNames, index + 1), 400)
          }, 800)
        })
      }
      function updateNumToDrawInputState() {
        const availableCount = Math.max(1, currentNames.length - pickedNames.size)
        DOM.numToDrawInput.max = availableCount
        if (parseInt(DOM.numToDrawInput.value, 10) > availableCount) {
          DOM.numToDrawInput.value = availableCount
        }
      }
      DOM.startBtn.addEventListener('click', () => {
        if (state !== 'idle') return
        const availableCount = currentNames.length - pickedNames.size
        if (availableCount === 0) {
          alert('所有人都被抽过啦！请重置。')
          return
        }
        state = 'gathering'
        rotationSound.play().catch(e => {})
        updateButtonStates(true, false)
        const initialFormation = formations[0]
        const duration = Math.max(500, settings.transformationSpeed * 0.5)
        nameSprites.forEach((sprite, i) => {
          const targetPosition = initialFormation(i, nameSprites.length)
          new TWEEN.Tween(sprite.position)
            .to(targetPosition, duration)
            .easing(TWEEN.Easing.Exponential.InOut)
            .start()
        })
        setTimeout(() => {
          if (state !== 'gathering') return
          state = 'rotating'
          if (settings.irregularRotation) {
            changeFormation()
            formationChangeInterval = setInterval(changeFormation, settings.transformationSpeed)
          }
        }, duration)
      })
      DOM.stopBtn.addEventListener('click', () => {
        if (state !== 'rotating' && state !== 'gathering') return
        clearInterval(formationChangeInterval)
        TWEEN.removeAll()
        rotationSound.pause()
        rotationSound.currentTime = 0
        const numToDraw = parseInt(DOM.numToDrawInput.value, 10)
        const availableSprites = nameSprites.filter(s => !s.userData.isPicked)
        if (availableSprites.length === 0) {
          alert('当前会话所有人都被点过啦！请重置。')
          returnToIdleState()
          return
        }
        if (numToDraw > availableSprites.length) {
          alert(`剩余人数（${availableSprites.length}）不足！`)
          returnToIdleState()
          return
        }
        state = 'stopping'
        updateButtonStates(true, false)
        const winners = availableSprites.sort(() => 0.5 - Math.random()).slice(0, numToDraw)
        const winnerNames = winners.map(w => w.userData.name)
        historyLog.push({names: winnerNames, time: Date.now()})
        winnerNames.forEach(name => pickedNames.add(name))
        allSessions[currentSessionTitle] = historyLog
        saveAllSessions()
        updateHistoryUI()
        updateNumToDrawInputState()
        new TWEEN.Tween(sphereGroup.rotation)
          .to({x: 0, y: 0, z: 0}, 1500)
          .easing(TWEEN.Easing.Exponential.Out)
          .start()
        nameSprites.forEach(sprite => {
          const isNewWinner = winnerNames.includes(sprite.userData.name)
          if (isNewWinner) {
            new TWEEN.Tween(sprite.position)
              .to({x: 0, y: 0, z: 0}, 1500)
              .easing(TWEEN.Easing.Exponential.Out)
              .start()
          } else {
            const direction = sprite.position.clone().normalize()
            if (direction.length() === 0) {
              direction
                .set(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5)
                .normalize()
            }
            const targetPosition = direction.multiplyScalar(50)
            new TWEEN.Tween(sprite.position)
              .to(targetPosition, 1500)
              .easing(TWEEN.Easing.Cubic.In)
              .start()
          }
        })
        setTimeout(() => {
          state = 'result'
          nameSprites.forEach(sprite => (sprite.visible = false))
          updateButtonStates(false, true)
          showWinnersSequentially(winnerNames)
        }, 1500)
      })
      DOM.resetBtn.addEventListener('click', () => {
        if (state === 'idle' && pickedNames.size > 0) {
          if (confirm(`您确定要清空【${currentSessionTitle}】下的所有抽取记录吗？`)) {
            historyLog = []
            pickedNames.clear()
            allSessions[currentSessionTitle] = []
            saveAllSessions()
            updateHistoryUI()
            updateNameSpritesState()
            updateNumToDrawInputState()
          }
        }
      })
      DOM.settingsBtn.addEventListener('click', () => {
        tempSettings = JSON.parse(JSON.stringify(settings))
        updatePanelFromTempSettings()
        DOM.modalOverlay.style.display = 'block'
        DOM.settingsPanel.style.display = 'flex'
      })
      ;[DOM.cancelSettingsBtn, DOM.modalOverlay].forEach(btn =>
        btn.addEventListener('click', () => {
          DOM.modalOverlay.style.display = 'none'
          DOM.settingsPanel.style.display = 'none'
          DOM.createDatasetModal.style.display = 'none'
        })
      )
      DOM.saveSettingsBtn.addEventListener('click', () => {
        applyAndSaveTempSettings()
        DOM.modalOverlay.click()
      })
      DOM.speechToggle.addEventListener('change', () => {
        tempSettings.speechEnabled = DOM.speechToggle.checked
      })
      DOM.glowToggle.addEventListener('change', () => {
        tempSettings.glowEnabled = DOM.glowToggle.checked
      })
      DOM.unpickedColorPicker.addEventListener('input', () => {
        tempSettings.unpickedColor = DOM.unpickedColorPicker.value
      })
      DOM.pickedColorPicker.addEventListener('input', () => {
        tempSettings.pickedColor = DOM.pickedColorPicker.value
      })
      DOM.bgColorPicker.addEventListener('input', () => {
        tempSettings.bgColor = DOM.bgColorPicker.value
      })
      DOM.rotationSpeedSlider.addEventListener('input', () => {
        tempSettings.rotationSpeed = parseInt(DOM.rotationSpeedSlider.value, 10)
      })
      DOM.irregularRotationToggle.addEventListener('change', () => {
        tempSettings.irregularRotation = DOM.irregularRotationToggle.checked
      })
      DOM.transformationSpeedSlider.addEventListener('input', () => {
        tempSettings.transformationSpeed = parseInt(DOM.transformationSpeedSlider.value, 10)
      })
      DOM.removeBgBtn.addEventListener('click', () => {
        tempSettings.bgImage = 'none'
        alert('背景图片已标记为移除，保存后生效。')
      })
      DOM.resetSettingsBtn.addEventListener('click', () => {
        if (confirm('确定要将所有外观和功能设置恢复为默认值吗？')) {
          tempSettings = JSON.parse(JSON.stringify(defaultSettings))
          updatePanelFromTempSettings()
        }
      })
      DOM.bgImageInput.addEventListener('change', e => {
        const file = e.target.files[0]
        if (file) {
          const reader = new FileReader()
          reader.onload = e => {
            tempSettings.bgImage = e.target.result
            alert('背景图片已选择，保存后生效。')
          }
          reader.readAsDataURL(file)
        }
      })
      DOM.numToDrawInput.addEventListener('input', () => {
        const value = parseInt(DOM.numToDrawInput.value, 10)
        const max = parseInt(DOM.numToDrawInput.max, 10)
        if (!isNaN(value) && value > max) {
          DOM.numToDrawInput.value = max
        }
      })
      DOM.numToDrawInput.addEventListener('blur', () => {
        const value = parseInt(DOM.numToDrawInput.value, 10)
        const min = parseInt(DOM.numToDrawInput.min, 10)
        if (isNaN(value) || value < min) {
          DOM.numToDrawInput.value = min
        }
      })
      DOM.persistToggle.addEventListener('change', () => {
        tempSettings.persistHistory = DOM.persistToggle.checked
        if (!tempSettings.persistHistory)
          alert('注意：关闭后，所有历史记录将在浏览器关闭后丢失。保存后生效。')
      })
      DOM.sessionTitleInput.addEventListener('change', () => {
        const newTitle = DOM.sessionTitleInput.value.trim()
        if (newTitle && newTitle !== currentSessionTitle) {
          if (!allSessions[newTitle]) {
            allSessions[newTitle] = []
            populateHistorySelect()
          }
          loadSession(newTitle)
        }
      })
      DOM.historySessionSelect.addEventListener('change', () => {
        loadSession(DOM.historySessionSelect.value)
      })
      DOM.deleteSessionBtn.addEventListener('click', () => {
        if (Object.keys(allSessions).length <= 1) {
          alert('无法删除唯一的会话！')
          return
        }
        if (confirm(`确定要永久删除会话【${currentSessionTitle}】及其所有记录吗？`)) {
          delete allSessions[currentSessionTitle]
          saveAllSessions()
          const remainingTitles = Object.keys(allSessions)
          loadSession(remainingTitles[0] || '默认会话')
          populateHistorySelect()
        }
      })
      DOM.openCreateDatasetModalBtn.addEventListener('click', () => {
        DOM.createDatasetModal.style.display = 'flex'
      })
      DOM.cancelCreateDatasetBtn.addEventListener('click', () => {
        DOM.createDatasetModal.style.display = 'none'
      })
      DOM.datasetSelect.addEventListener('change', () => {
        const selectedName = DOM.datasetSelect.value
        setCurrentDataset(selectedName)
      })
      DOM.createDatasetBtn.addEventListener('click', () => {
        const name = DOM.newDatasetNameInput.value.trim()
        const namesStr = DOM.newDatasetNamesInput.value.trim()
        if (!name) {
          alert('请输入数据集名称！')
          return
        }
        if (allDatasets[name]) {
          alert('该名称已存在！')
          return
        }
        if (!namesStr) {
          alert('请输入名单！')
          return
        }
        const names = namesStr.split(/[\s\n,，]+/).filter(n => n.trim() !== '')
        if (names.length === 0) {
          alert('未检测到有效的名字！')
          return
        }
        allDatasets[name] = {isPreset: false, names: names}
        saveCustomDatasets()
        populateDatasetSelect()
        setCurrentDataset(name)
        DOM.newDatasetNameInput.value = ''
        DOM.newDatasetNamesInput.value = ''
        DOM.createDatasetModal.style.display = 'none'
      })
      DOM.deleteDatasetBtn.addEventListener('click', () => {
        const selectedName = DOM.datasetSelect.value
        if (allDatasets[selectedName].isPreset) {
          alert('无法删除预置数据集！')
          return
        }
        if (confirm(`确定要永久删除数据集【${selectedName}】吗？`)) {
          if (settings.defaultDataset === selectedName) {
            settings.defaultDataset = Object.keys(PRESET_DATASETS)[0]
            localStorage.setItem('namePickerSettings', JSON.stringify(settings))
          }
          delete allDatasets[selectedName]
          saveCustomDatasets()
          populateDatasetSelect()
          setCurrentDataset(settings.defaultDataset)
        }
      })
      DOM.setDefaultDatasetBtn.addEventListener('click', () => {
        const selectedName = DOM.datasetSelect.value
        tempSettings.defaultDataset = selectedName
        settings.defaultDataset = selectedName
        localStorage.setItem('namePickerSettings', JSON.stringify(settings))
        updateDatasetButtons()
        alert(`数据集“${selectedName}”已设为默认。`)
      })
      DOM.importXlsxBtn.addEventListener('click', () => {
        const file = DOM.xlsxFileInput.files[0]
        const colRefsStr = DOM.xlsxColumnInput.value.trim().toUpperCase()
        const filterStr = DOM.xlsxFilterInput.value.trim()
        if (!file) {
          alert('请选择Excel文件')
          return
        }
        if (!colRefsStr) {
          alert('请填写要导入的列标（如 A,B）')
          return
        }
        const colRefs = colRefsStr
          .split(',')
          .map(c => c.trim())
          .filter(c => c)
        if (colRefs.some(c => !/^[A-Z]+$/.test(c))) {
          alert('列标格式不正确，请使用逗号分隔的字母组合（如 A,B,AA）')
          return
        }
        const filterWords = filterStr
          ? new Set(
              filterStr
                .split(',')
                .map(w => w.trim())
                .filter(w => w)
            )
          : new Set()
        const reader = new FileReader()
        reader.onload = function (e) {
          try {
            const data = new Uint8Array(e.target.result)
            const workbook = XLSX.read(data, {type: 'array'})
            const firstSheetName = workbook.SheetNames[0]
            if (!firstSheetName) {
              alert('Excel文件中没有工作表')
              return
            }
            const worksheet = workbook.Sheets[firstSheetName]
            const names = new Set()
            const range = XLSX.utils.decode_range(worksheet['!ref'])
            colRefs.forEach(colRef => {
              for (let row = range.s.r; row <= range.e.r; ++row) {
                const cellAddress = colRef + (row + 1)
                const cell = worksheet[cellAddress]
                if (cell && cell.v !== undefined && cell.v !== null) {
                  const cellValue = ('' + cell.v).trim()
                  if (cellValue && !filterWords.has(cellValue)) {
                    names.add(cellValue)
                  }
                }
              }
            })
            if (names.size === 0) {
              alert('在指定列中未检索到有效数据，或所有数据均被过滤')
              return
            }
            DOM.newDatasetNamesInput.value = [...names].join('\n')
          } catch (error) {
            console.error('Error processing Excel file:', error)
            alert('处理Excel文件时发生错误，请检查文件格式是否正确。')
          }
        }
        reader.readAsArrayBuffer(file)
      })
      function loadInitialData() {
        const savedSettings = localStorage.getItem('namePickerSettings')
        if (savedSettings) settings = {...defaultSettings, ...JSON.parse(savedSettings)}
        initScene()
        loadAllDatasets()
        if (!allDatasets[settings.defaultDataset]) {
          settings.defaultDataset = Object.keys(PRESET_DATASETS)[0]
          localStorage.setItem('namePickerSettings', JSON.stringify(settings))
        }
        setCurrentDataset(settings.defaultDataset)
        applySettingsUI()
      }
      loadInitialData()
      animate()
    </script>
  </body>
</html>
